import fs from 'node:fs';
import path from 'node:path';

import chalk from 'chalk';
import { resolve } from 'import-meta-resolve';
import tsj from 'ts-json-schema-generator';

import { shouldQuote } from '@nangohq/nango-yaml';

import { TYPES_FILE_NAME } from '../constants.js';
import { printDebug } from '../utils.js';
import { parse } from './config.service.js';
import { NANGO_VERSION } from '../version.js';

import type { NangoModel, NangoModelField, NangoYamlParsed } from '@nangohq/types';
import type { JSONSchema7 } from 'json-schema';

export type ModelsMap = Map<string, Record<string, any>>;

/**
 * Load nango.yaml and generate model.ts
 */
export function loadYamlAndGenerate({ fullPath, debug = false }: { fullPath: string; debug?: boolean }): NangoYamlParsed | null {
    if (debug) {
        printDebug(`Generating ${TYPES_FILE_NAME} file`);
    }

    const fp = path.resolve(fullPath, TYPES_FILE_NAME);
    if (!fs.existsSync(fp)) {
        if (debug) {
            printDebug('First compilation');
        }
    } else {
        if (debug) {
            printDebug(`File already exists, replacing`);
        }
    }

    const parsing = parse(fullPath, debug);
    if (parsing.isErr()) {
        console.log(chalk.red(parsing.error.message));
        return null;
    }

    const modelTs = buildModelsTS({ parsed: parsing.value.parsed! });
    fs.writeFileSync(fp, modelTs);

    if (debug) {
        printDebug(`${TYPES_FILE_NAME} generated`);
    }

    // --- Additional exports
    generateAdditionalExports({ parsed: parsing.value.parsed!, fullPath, debug });

    return parsing.value.parsed!;
}

export function loadSchemaJson({ fullPath }: { fullPath: string }): JSONSchema7 | null {
    const filePath = path.join(fullPath, '.nango', 'schema.json');
    try {
        return JSON.parse(fs.readFileSync(filePath).toString()) as JSONSchema7;
    } catch (err) {
        console.error(chalk.red(`Error loading ${filePath}`), err);
        return null;
    }
}

/**
 * Build models.ts
 */
export function buildModelsTS({ parsed }: { parsed: NangoYamlParsed }): string {
    return `// ---------------------------
// This file was generated by Nango (v${NANGO_VERSION})
// It's recommended to version this file
// https://nango.dev
// ---------------------------

// ------ Models
${generateInterfaces({ parsed }).join('\n\n')}
// ------ /Models

// ------ SDK
${generateSDKTypes()}
// ------ /SDK

// ------ Flows
export const NangoFlows = ${JSON.stringify(parsed.integrations, null, 2)} as const;
// ------ /Flows
`;
}

export function generateInterfaces({ parsed }: { parsed: NangoYamlParsed }): string[] {
    const interfaces: string[] = [];
    for (const [, model] of parsed.models) {
        interfaces.push(modelToTypescript({ model }));
    }

    return interfaces;
}

/**
 * Transform a JSON model to its Typescript equivalent
 */
export function modelToTypescript({ model }: { model: NangoModel }) {
    const output: string[] = [];
    if (model.isAnon) {
        output.push(`export type ${model.name} = ${fieldToTypescript({ field: model.fields[0]! })}`);
    } else {
        output.push(`export interface ${model.name} {`);
        output.push(...fieldsToTypescript({ fields: model.fields }));
        output.push(`};`);
    }
    return output.join('\n');
}

export function fieldsToTypescript({ fields }: { fields: NangoModelField[] }) {
    const output: string[] = [];
    const dynamic = fields.find((field) => field.dynamic);

    // Insert dynamic key at the beginning
    if (dynamic) {
        output.push(`  [key: string]: ${fieldToTypescript({ field: dynamic })};`);
    }

    // Regular fields
    for (const field of fields) {
        if (field.dynamic) {
            continue;
        }

        output.push(`  ${shouldQuote(field.name) ? `"${field.name}"` : field.name}${field.optional ? '?' : ''}: ${fieldToTypescript({ field: field })};`);
    }

    return output;
}

/**
 * Transform a field definition to its typescript equivalent
 */
export function fieldToTypescript({ field }: { field: NangoModelField }): string | boolean | null | undefined | number {
    let output = '';
    if (Array.isArray(field.value)) {
        if (field.union) {
            output = field.value.map((f) => fieldToTypescript({ field: f })).join(' | ');
        } else {
            if (!field.tsType && field.array) {
                output = field.value.map((f) => fieldToTypescript({ field: f })).join(' | ');
            } else {
                // Due to bad decision an object is an array of values without any other flag
                output = `{${fieldsToTypescript({ fields: field.value }).join('\n')}}`;
            }
            if (field.array) {
                output = `(${output})[]`;
            }
        }
    } else if (field.model || field.tsType) {
        output = `${field.value}${field.array ? '[]' : ''}`;
    } else if (field.value === null) {
        output = 'null';
    } else if (typeof field.value === 'string') {
        output = `'${field.value}${field.array ? '[]' : ''}'`;
    } else {
        output = `${field.value}${field.array ? '[]' : ''}`;
    }

    if (field.optional && !output.includes('undefined')) {
        output = `${output} | undefined`;
    }

    return output;
}

/**
 * Generate SDK types
 */
export function generateSDKTypes() {
    const filePath = resolve('@nangohq/runner-sdk/models.d.ts', import.meta.url);
    const typesContent = fs.readFileSync(new URL(filePath), 'utf8');

    return `
${typesContent}

`;
}

export function generateAdditionalExports({ fullPath, parsed, debug }: { fullPath: string; parsed: NangoYamlParsed; debug: boolean }): void {
    const exportPath = path.resolve(fullPath, '.nango');
    if (debug) {
        printDebug(`Generating schemas in ${exportPath}`);
    }
    if (!fs.existsSync(exportPath)) {
        fs.mkdirSync(exportPath, { recursive: true });
    }

    // Standalone typescript schema
    const pathTS = path.join(exportPath, 'schema.ts');
    fs.writeFileSync(pathTS, getExportToTS({ parsed }));
    if (debug) {
        printDebug(`Generated export ${pathTS}`);
    }

    // Standalone json schema
    const pathJSON = path.join(exportPath, 'schema.json');
    fs.writeFileSync(pathJSON, JSON.stringify(getExportToJSON({ pathTS }), null, 2));
    if (debug) {
        printDebug(`Generated export ${pathJSON}`);
    }

    fs.writeFileSync(path.join(exportPath, 'nango.json'), JSON.stringify(parsed.integrations, null, 2));
}

export function getExportToTS({ parsed }: { parsed: NangoYamlParsed }): string {
    return `// ---------------------------
// This file was generated by Nango (v${NANGO_VERSION})
// You can version this file
// ---------------------------

${generateInterfaces({ parsed }).join('\n\n')}
`;
}

export function getExportToJSON({ pathTS }: { pathTS: string }): JSONSchema7 {
    const schema = tsj
        .createGenerator({
            path: pathTS,
            type: '*',
            skipTypeCheck: true
        })
        .createSchema('*');

    schema.$comment = `This file was generated by Nango (v${NANGO_VERSION})`;

    // When there only 1 model it appends this useless $ref
    delete schema.$ref;

    return schema;
}
